# Copyright (c) 2007 Fabio Makoto Akita
# 
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# Author: AkitaOnRails (www.akitaonrails.com)
# Support: Surgeworks LLC (www.surgeworks.com)
module ActsAsReplica
  
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def sync_options=(d = {})
      d.delete(:root)
      super(d)
    end

    # this is the default configuration loader
    # if the SyncSettings table is empty, it creates the initial and
    # unique configuration for this machine
    # it is important for its uniqueness because each machine will have
    # a unique machine_id UUID
    def load_config
      # Push in default values
      setting = SyncSetting.load
      
      cfg = {}
      setting.attributes.each { |k,v| cfg.merge!( k.to_sym => v ) }
      cfg
    end
    
    # Enables the replication capabilities of the model
    def acts_as_replica(options = {})
      has_many :replicas, :as => :method # polymorphic association
  
      after_create :sync_create
      after_update :sync_update
      after_destroy :sync_destroy

      cattr_accessor :sync_options
      self.sync_options = options

      acts_as_uuid
      acts_as_audited
      include ActsAsReplica::InstanceMethods
    end
    
    # only generates a new UUID is there isn't one already assigned
    # uses the internal self[:id] instead of self.id because for some
    # unknown reason, the .id proxy doesn't show an already assigned
    # value there before saving the new row
    def acts_as_uuid
      before_create :generate_uuid
      
      define_method(:generate_uuid) do
        self.id = self[:id] || UUID.timestamp_create().to_s
      end
    end
    
    # To be used in Model classes that will receive the UserStamp
    def acts_as_audited
      belongs_to :created, :class_name => "User", :foreign_key => "created_by"
      belongs_to :updated, :class_name => "User", :foreign_key => "updated_by"
  
      define_method(:to_s) do
        fullname if respond_to? :fullname
      end
    end

    # To be used in the User class
    def acts_as_auditor
      before_save :set_last_synced
      before_create :generate_guid
      after_save :generate_remote_client
      
      cattr_accessor :current_user
      
      acts_as_replica
      include ActsAsReplica::AuditorInstanceMethods
    end

  end

  module InstanceMethods
    protected 
      def sync_create
        Replica.add(Replica::METHOD_CREATE, self) if Replica.track_replica
      end

      def sync_update
        Replica.add(Replica::METHOD_UPDATE, self) if Replica.track_replica
      end

      def sync_destroy
        if self.respond_to?("deleted_attribute") # if use acts_as_paranoid, need to be this
          Replica.add(Replica::METHOD_UPDATE, self.class.find_with_deleted(self.id)) if Replica.track_replica
        else
          Replica.add(Replica::METHOD_DESTROY, self) if Replica.track_replica
        end
      end
  end 
  
  module AuditorInstanceMethods
    def last_synced
      @last_synced ||= SyncSetting.find(:first).last_synced || -1
    end
    
    def last_synced=(value)
      @last_synced = value
    end

    private
    
    def set_last_synced 
      SyncSetting.find(:first).update_attributes(:last_synced => last_synced)
    end

    def generate_guid
      self.guid = UUID.timestamp_create().to_s if new_record? && self.guid.blank?
    end

    def generate_remote_client
      RemoteClient.create :client_id => self[:id], :guid => self.guid
    end
  end

  # These are the packages that will be dealed back and forth between the client 
  # and server synchronization negotiation.
  module Structs
    # This is the package that will be serialized with a lot of SyncTransaction
    # instances holding the modified table rows in the payload field
    class SyncPayload
      attr_accessor :payload, :version, :errors, :total, :last_synced, :status, :result_message
      def initialize(options = {})
        self.payload         = options[:payload] || []
        self.version         = options[:version] || 1.0
        self.errors          = options[:errors] || []
        self.total           = options[:total] || self.payload.size
        self.last_synced     = options[:last_synced] || -1
        self.status          = options[:status] || 200
        self.result_message  = options[:result_message] || ""
      end
      
      def self.build_empty(count = 0)
        ActsAsReplica::Structs::SyncPayload.new(:payload => count.to_s,
                        :version      => Replica.version, 
                        :errors       => [],
                        :total        => count.to_s,
                        :last_synced  => Replica.maximum('id'))
      end
    end

    # The instance of this class holds which row of each table suffered what CRUD operation
    class SyncTransaction
      attr_accessor :counter, :method, :sync_id, :row, :destroy_id
      def initialize(options = {})
        self.counter    = options[:counter] || -1
        self.method     = options[:method] || nil
        self.sync_id    = options[:sync_id] || -1
        self.row        = options[:row] || nil
        self.destroy_id = options[:destroy_id] || -1
      end
    end

    # Once the modifications are made to the database, this returns the errors
    # that could have been generated (usually none) and the last updated timestamp
    # for future reference
    class SyncUpResult
      attr_accessor :errors, :last_synced_id, :total, :status
      def initialize(options = {})
        self.errors          = options[:errors] || []
        self.last_synced_id  = options[:last_synced_id] || -1
        self.total           = options[:total] || 0
        self.status          = options[:status] || 200
      end
    end
    
    class SyncDumpResult < SyncUpResult
      attr_accessor :last_synced, :result_message
      def initialize(options = {})
        super(options)
        self.last_synced = options[:last_synced] || -1
        self.result_message = options[:result_message] || ""
      end
    end
  end
end 

# Extends the ActiveRecord::Base to comply with UserStamp plugin requirements
# This is a triade: a Auditor, one or more Audited models and a Userstamp controller filter
# 
# == Instructions
# 
# at the end of config/environment.rb add:
# require 'userstamp_helper'
# 
# class User
#   acts_as_auditor
#   ...
# end
# 
# class Model
#   acts_as_audited
#   ...
# end
# 
# class ApplicationController < ActionController::Base
#   acts_as_userstamp
#   ...
# end
module ActionController
  module UserStamp
    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods
      def acts_as_userstamp
        before_filter do |c|
          begin
            if User.current_user.nil? || User.current_user.id.to_s != c.session[:user].to_s
              User.current_user = User.find(c.session[:user])
            end
          rescue ActiveRecord::RecordNotFound => e
            c.session[:user] = nil
            User.current_user = nil
          end
        end
      end
    end
  end
end